from google.appengine.ext import db

import validation

from pybcryptmaster import bcrypt
from validation import string_length_validator, string_format_validator, \
                       combine_validators



#### Constants ####

BCRYPT_ROUNDS = 5

# Matches a non-empty string of word characters (letters, digits and '_')
WORD_REGEX = "^(\w)*$"

LIMITS = {
    "username_min_length":  1,
    "username_max_length":  25,
    "username_valid_regex": WORD_REGEX,
    
    "password_min_length":  5,
    "password_max_length":  25,
    "password_valid_regex": WORD_REGEX,
    
    "email_min_length":  1,
    "email_max_length":  60,
    "email_valid_regex": "^[A-Za-z0-9\.\+_-]+@[A-Za-z0-9\._-]+\.[a-zA-Z]*$",
    "email_required":    True,
    
    "name_min_length": 1,
    "name_max_length": 45,
    "name_required":   False,
    
    "surname_min_length": 1,
    "surname_max_length": 45,
    "surname_required":   False,
    
    "date_of_birth_required": False
}

NOT_DEFINED_MESSAGE = "%s is required."
INVALID_LENGTH_MESSAGE = "%s must be between %d and %d characters long."
WORD_MESSAGE = "%s can consist only of letters, numbers and _."

ERROR_MESSAGES = {
    "username_not_unique":  "This username has already been taken. Choose another one.",
    "username_not_defined":    NOT_DEFINED_MESSAGE % ("Username"),
    "username_invalid_format": WORD_MESSAGE % ("Username"),
    "username_invalid_length": INVALID_LENGTH_MESSAGE %
        ("Username", LIMITS["username_min_length"], LIMITS["username_max_length"]),
    
    "password_not_defined":    NOT_DEFINED_MESSAGE % ("Password"),
    "password_invalid_format":  WORD_MESSAGE % ("Password"),
    "password_invalid_length": INVALID_LENGTH_MESSAGE %
        ("Password", LIMITS["password_min_length"], LIMITS["password_max_length"]),
    
    "password_repeat_equal_to":    "Passwords don't match.",
    "password_repeat_not_defined": "You must repeat password.",
    
    "email_not_unique":  "There already is a user registered to this email.",
    "email_not_defined":    NOT_DEFINED_MESSAGE % ("Email"),
    "email_invalid_format": "Enter a valid email address.",
    "email_invalid_length": INVALID_LENGTH_MESSAGE %
        ("Email", LIMITS["email_min_length"], LIMITS["email_max_length"]),
    
    "name_not_defined":    NOT_DEFINED_MESSAGE % ("Name"),
    "name_invalid_length": INVALID_LENGTH_MESSAGE %
        ("Name", LIMITS["name_min_length"], LIMITS["name_max_length"]),
    
    "surname_not_defined":    NOT_DEFINED_MESSAGE % ("Surname"),
    "surname_invalid_length": INVALID_LENGTH_MESSAGE %
        ("Surname", LIMITS["surname_min_length"], LIMITS["surname_max_length"]),
    
    "date_of_birth_not_defined": NOT_DEFINED_MESSAGE % ("Date of birth"),
    "date_of_birth_invalid":     "Enter a valid date (format: yyyy-mm-dd)."
}


#### Validation exceptions ####

class UsernameNotUnique(validation.PropertyNotUnique):
    def __str__(self):
        return "\"" + self.value + "\" already taken"

class UsernameInvalidLength(validation.StringInvalidLength):
    pass

class UsernameInvalidFormat(validation.StringInvalidFormat):    
    def __str__(self):
        return "Only alphanumeric characters allowed"

class PasswordInvalidLength(validation.StringInvalidLength):
    pass

class PasswordInvalidFormat(validation.StringInvalidFormat):    
    def __str__(self):
        return "Only alphanumeric characters allowed"

class EmailNotUnique(validation.PropertyNotUnique):
    def __str__(self):
        return "There already is a user registered to " + self.value

class EmailInvalidLength(validation.StringInvalidLength):
    pass

class EmailInvalidFormat(validation.StringInvalidFormat):
    pass

class NameInvalidLength(validation.StringInvalidLength):
    pass

class SurnameInvalidLength(validation.StringInvalidLength):
    pass


#### Models ####

class PasswordProperty(db.StringProperty):
    """
    Used to store users' passwords.
    
    Password is constructed using password string, but stored in the datastore
    as a salted hash value. Hash value is calculated using bcrypt algorithm.
    
    Before assigning a value to the property, the value must be validated using
    validate_password()

    """
    
    data_type = basestring
    validator = combine_validators(
        string_length_validator(
            LIMITS["password_min_length"],
            LIMITS["password_max_length"],
            PasswordInvalidLength
        ),
        string_format_validator(
            LIMITS["password_valid_regex"],
            PasswordInvalidFormat
        )
    )
    
    def get_value_for_datastore(self, model_instance):
        password = \
            super(PasswordProperty, self).get_value_for_datastore(model_instance)
        return bcrypt.hashpw(password, bcrypt.gensalt(BCRYPT_ROUNDS))
    
    def make_value_from_datastore(self, value):
        return value
    
    @classmethod
    def validate_password(class_obj, password):
        """
        Validates given password. If password is valid it terminates normally.
        Otherwise raises PasswordInvalidLength and PasswordInvalidFormat
        exceptions.
        
        This validation can't be done inside of a regular property validator
        (validate() method), because that validation is done on a value we store
        in the database (salted hash value) instead of a source password string
        which we want to validate.
        
        """ 
        class_obj.validator(password)


class User(db.Model):
    """
    User account in the application.
    
    Before creating instance of this class, you are obliged to check for
    username availability, check for email availability and validate password.
    
    Creating instance of this class throws exceptions:
    UsernameInvalidLength, UsernameInvalidFormat, EmailInvalidLength,
    EmailInvalidFormat, NameInvalidLength, SurnameInvalidLength
    
    """
    
    ### Validators ###

    _username_validator = combine_validators(
        string_length_validator(
            LIMITS["username_min_length"],
            LIMITS["username_max_length"],
            UsernameInvalidLength
        ),
        string_format_validator(
            LIMITS["username_valid_regex"],
            UsernameInvalidFormat
        )
    )
    
    _email_validator = combine_validators(
        string_length_validator(
            LIMITS["email_min_length"],
            LIMITS["email_max_length"],
            EmailInvalidLength),
        string_format_validator(
            LIMITS["email_valid_regex"],
            EmailInvalidFormat
        )
    )
    
    _name_validator = string_length_validator(
        LIMITS["name_min_length"],
        LIMITS["name_max_length"],
        NameInvalidLength
    )
    
    _surname_validator = string_length_validator(
        LIMITS["surname_min_length"],
        LIMITS["surname_max_length"],
        SurnameInvalidLength
    )
    
    ### Fields ###
    
    ## Personal details ##
    username = db.StringProperty(required=True, validator=_username_validator)
    password = PasswordProperty(required=True)
    email = db.EmailProperty(required=LIMITS["email_required"],
                             validator=_email_validator)
    name = db.StringProperty(required=LIMITS["name_required"],
                             validator=_name_validator)
    surname = db.StringProperty(required=LIMITS["surname_required"],
                                validator=_surname_validator)
    date_of_birth = db.DateProperty(required=LIMITS["date_of_birth_required"])
    
    ## Metadata ##
    # Is this user account deleted?
    is_deleted = db.BooleanProperty(required=True, default=False)
    # Time of the last user activity on the page.
    last_visit = db.DateTimeProperty(required=False, default=None)
    # Time of the last change to the user account.
    last_change = db.DateTimeProperty(required=True, auto_now=True)
    # Time of the user account creation.
    insert_time = db.DateTimeProperty(required=True, auto_now_add=True)
    
    ## Methods ##
    
    @classmethod
    def _check_property_uniqueness(class_obj, property_name, property_value, not_unique_exception):
        query = class_obj.all().filter(property_name, property_value)
        if (query.get()):
            raise not_unique_exception(property_value)
    
    @classmethod
    def check_username_availability(class_obj, username):
        """
        Username availability must be checked before creating User instance with
        that username. Throws UsernameNotUnique exception.
        
        """
        class_obj._check_property_uniqueness("username", username, UsernameNotUnique)
    
    @classmethod
    def check_email_availability(class_obj, email):
        """
        Email availability must be checked before creating User instance with
        that email. Throws EmailNotUnique exception.
        
        """
        class_obj._check_property_uniqueness("email", email, EmailNotUnique)
    
    @classmethod
    def validate_username(class_obj, username):
        """Throws UsernameInvalidLength and UsernameInvalidFormat."""
        class_obj._username_validator(username)
    
    @classmethod
    def validate_password(class_obj, password):
        """
        Password must be validated before creating User instance using that
        password. Throws PasswordInvalidLength and PasswordInvalidFormat.
        
        """
        class_obj.password.validate_password(password)
    
    @classmethod
    def validate_email(class_obj, email):
        """Throws EmailInvalidLength and EmailInvalidFormat"""
        
        class_obj._email_validator(email)
    
    @classmethod
    def validate_name(class_obj, name):
        """Throws NameInvalidLength."""
        
        class_obj._name_validator(name)
    
    @classmethod
    def validate_surname(class_obj, surname):
        """Throws SurnameInvalidLength."""
        
        class_obj._surname_validator(surname)
    
    def check_password(self, password):
        """
        Returns if a given password matches the password defined for this
        user.
        
        """
        return bcrypt.hashpw(password, self.password) == self.password
    
    def __str__(self):
        return "User " + self.username
    
    def __eq__(self, value): 
        if self.__class__ == value.__class__: 
            return self.key() == value.key() 
        else: 
            return False 


class PasswordRecoveryRequest(db.Model):
    """
    Created on user's request to reset his password. It's key is a token that
    is sent to user via email and used to identify the request.
    
    """
    user = db.ReferenceProperty(reference_class=User, required=True)